'use strict'; const { Document } = require('/document.js'); const { DocumentCommand, CompoundCommandBuilder } = require('/commands.js'); const { CurveBuilder, PolyCurve } = require('/geometry.js'); const { Dialog, DialogResult, UnitType } = require('/dialog.js'); const { app } = require('/application.js'); function abort(msg, alert = true) { if (alert) app.alert(msg); throw new Error(msg); } function isOk(r) { return (r?.value ?? r) === DialogResult.Ok.value; } const doc = Document.current; if (!doc) abort('This script requires an open document.'); const spread = doc.currentSpread; // Capture the selected curves BEFORE the dialog const nodes = doc.selection.nodes.filter(n => n.isPolyCurveNode).toArray(); if (nodes.length === 0) abort('Select one or more curves first.'); // ---- Dialog ---- const dlg = Dialog.create('Random Node Shift'); const g1 = dlg.addColumn().addGroup('** Requires curves to be selected'); const minEd = g1.addUnitValueEditor('Min shift (%)', UnitType.Number, UnitType.Number, 0, 0, 100).setPrecision(1); const maxEd = g1.addUnitValueEditor('Max shift (%)', UnitType.Number, UnitType.Number, 10, 0, 100).setPrecision(1); dlg.initialWidth = 320; const result = dlg.runModal(); if (!isOk(result)) abort('Cancelled.', false); // Dialog resets the spread context, so set it again AFTER it closes doc.executeCommand(DocumentCommand.createSetCurrentSpread(spread)); // Read the values once, as plain numbers let lo = minEd.value / 100; let hi = maxEd.value / 100; if (lo > hi) { const t = lo; lo = hi; hi = t; } // Overall size of a curve = diagonal of the box around its nodes function nodeDiag(poly) { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const c of poly) for (const b of c.beziers) for (const p of [b.start, b.end]) { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); } return isFinite(minX) ? Math.hypot(maxX - minX, maxY - minY) : 0; } // Move every node a random distance (lo..hi of the curve size) in a random direction function shiftCurve(curve, diag) { const bezs = []; for (const b of curve.beziers) bezs.push(b); if (bezs.length === 0) return null; const n = bezs.length; const first = bezs[0].start, last = bezs[n - 1].end; // A closed curve whose last point sits on its first point shares one node const joined = curve.isClosed && Math.hypot(last.x - first.x, last.y - first.y) < 1e-6; const off = []; // one random offset per node (n + 1 nodes) for (let k = 0; k <= n; k++) { if (joined && k === n) { off.push(off[0]); continue; } const d = (lo + Math.random() * (hi - lo)) * diag; const a = Math.random() * Math.PI * 2; off.push({ x: Math.cos(a) * d, y: Math.sin(a) * d }); } const builder = CurveBuilder.create(); builder.begin({ x: first.x + off[0].x, y: first.y + off[0].y }); for (let i = 0; i < n; i++) { const b = bezs[i], o0 = off[i], o1 = off[i + 1]; // handles travel with the node they belong to, so smooth nodes stay smooth builder.addBezierXY(b.c1.x + o0.x, b.c1.y + o0.y, b.c2.x + o1.x, b.c2.y + o1.y, b.end.x + o1.x, b.end.y + o1.y); } if (curve.isClosed) builder.close(); return builder.createCurve(); } function shiftPoly(poly) { const diag = nodeDiag(poly); const out = PolyCurve.create(); for (const c of poly) { const nc = shiftCurve(c, diag); if (nc) out.addCurve(nc); } return out; } // ---- Apply everything as ONE command = ONE undo step ---- const compound = CompoundCommandBuilder.create(); let count = 0; for (const node of nodes) { const newPoly = shiftPoly(node.polyCurve); if (newPoly.curveCount > 0) { compound.addCommand(DocumentCommand.createSetCurves(node.curvesInterface, newPoly)); count++; } } if (count === 0) abort('Nothing to shift.'); doc.executeCommand(compound.createCommand()); console.log(`Done! Shifted nodes on ${count} curve(s) by ${(lo * 100).toFixed(1)}–${(hi * 100).toFixed(1)}% of curve size.`);